package fast;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.sql.SQLException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * Decision making engine for the master, receiving 
 * input from a user interface then passing them 
 * onto slave processors.
 * 
 * @author Adam Bell
 */
public class MasterProcessor extends Thread {
	private String userID;
	private int SID = 0;
	private int FID = 0;
	private String filePath = "";

	private int commandID;

	private Thread mTalker;
	private Thread mListen;
	private Thread mUDPTalker;
	private Thread innerDataBaseWriter;
	private Buffer mTalkBuffer;
	private Buffer mListenBuffer;
	private Buffer mUDPTalkBuffer;
	private Buffer resultsBuffer;
	public PeerManager peerMgr;
	public StateManager stateMgr;
	private JDBC databaseMgr;

	private static MasterProcessor mp;

	private final int TCP_LISTEN_PORT = 5000;
	private final int UDP_LISTEN_PORT = 6000;
	private final int BUFFER_LEN = 5000;

	private boolean running = true;

	/**
	 * Constructor not publicly available. Use getMasterProcessor instead.
	 * 
	 * @throws IOException
	 * @throws FileNotFoundException
	 * 
	 */
	private MasterProcessor() throws FileNotFoundException, IOException {

		FilePath fp = new FilePath();
		filePath = fp.getFilePath();

		printLogMessage("path = " + fp.getFilePath());

		// prep some buffers, listeners, talkers, JDBC & peer manager
		this.mTalkBuffer = new Buffer(BUFFER_LEN);
		this.mListenBuffer = new Buffer(BUFFER_LEN);
		this.mUDPTalkBuffer = new Buffer(BUFFER_LEN);
		this.resultsBuffer = new Buffer(BUFFER_LEN);

		this.mTalker = new Thread(new TCPTalker(mTalkBuffer));
		this.mListen = new Thread(new TCPListener(TCP_LISTEN_PORT,
				mListenBuffer));
		this.mUDPTalker = new Thread(new UDPTalker(mUDPTalkBuffer));
		this.innerDataBaseWriter = new Thread(this.new InnerDataBaseWriter(
				resultsBuffer));

		this.databaseMgr = new JDBC();
		this.stateMgr = new StateManager();
		this.peerMgr = new PeerManager();

		// initialise variables
		this.commandID = 0;
	}

	/**
	 * Inner class to handle writing to a database. Will be run as a thread.
	 * 
	 * @author Adam Bell
	 * 
	 */
	private class InnerDataBaseWriter implements Runnable {

		Buffer b;

		public InnerDataBaseWriter(Buffer b) {
			this.b = b;
		}

		/**
		 * Thread actions
		 */
		public void run() {

			String[][] dataArray;

			while (true) {
				try {

					if (b.getCount() > 10) {
						// remove 10 records from the buffer
						dataArray = new String[10][2];
						for (int i = 0; i < 10; i++) {
							String[] removed = b.remove().split(",");
							dataArray[i][0] = removed[0];
							dataArray[i][1] = removed[1];
						}
					} else {
						// remove 1 record from the buffer
						dataArray = new String[1][2];
						String[] removed = b.remove().split(",");
						dataArray[0][0] = removed[0];
						dataArray[0][1] = removed[1];
					}

					// add to the database
					if (databaseMgr.openDB()) {
						databaseMgr.resultsWrite(dataArray, SID, FID);
					}

				} catch (InterruptedException e) {
					printLogMessage(e.toString());
				} catch (SQLException e) {
					printLogMessage(e.toString());
				} catch (Exception e) {
					printLogMessage(e.toString());
				} finally {
					printLogMessage("Results entered into database: results and resultsCombined");
				}
			}
		}
	}// inner class

	/**
	 * Public constructor for MasterProcessor. Implements singleton design
	 * pattern.
	 * 
	 * @return Singleton instance of MasterProcessor
	 * @throws FileNotFoundException
	 * @throws IOException
	 */
	public static MasterProcessor getMasterProcessor()
			throws FileNotFoundException, IOException {
		if (mp == null) {
			mp = new MasterProcessor();
		}

		return mp;
	}

	/**
	 * Thread actions
	 */
	public void run() {
		// kickoff the talker, listener and databasewriter threads
		this.mTalker.start();
		this.mListen.start();
		this.mUDPTalker.start();
		this.innerDataBaseWriter.start();

		sendPeer();

		// run until halted
		while (this.running) {
			try {
				// remove an entry from the listenbuffer
				String bufferString = mListenBuffer.remove();

				// split the string, token on ":". [0] = ip, [1], = id, [2] =
				// command, [3] = data
				String[] parsed = bufferString.split("\\|");

				String ip = parsed[0];
				String id = parsed[1];
				String command = parsed[2];
				String data = "";

				if (parsed.length > 3) {
					data = parsed[3];
				}

				// Decision maker based on received command
				if (command.equals(CommandsEnum.SYSTEM_JOIN.toString())) {
					sysJoin(ip, id, data);
				} else if (command.equals(CommandsEnum.SYSTEM_LEAVE.toString())) {
					sysLeave(ip);
				} else if (command.equals(CommandsEnum.SYSTEM_DONE.toString())) {
					sysDone(ip, id);
				} else if (command.equals(CommandsEnum.SYSTEM_ERROR.toString())) {
					sysError(ip, id, data);
				} else if (command.equals(CommandsEnum.PROC_RESULT.toString())) {
					procResult(ip, data);
				} else if (command
						.equals(CommandsEnum.PROC_COMPLETE.toString())) {
					procComplete(ip, id, data);
				} else {
					// invalid command, do nothing
				}
			} catch (InterruptedException e) {
				printLogMessage(e.toString());
			}

		}

	}

	/**
	 * Unused method for user logins, this is now handled in the UI
	 * 
	 * @param userID
	 */
	@Deprecated
	public void login(String userID) {
		// dont do any login stuff
		this.userID = userID;

		printLogMessage("Successful login for user " + userID);
	}

	/**
	 * Unused method for user logouts, this is now handled in the UI
	 */
	@Deprecated
	public void logout() {
		// or logout stuff
		this.userID = null;
	}

	/**
	 * Send a UDP broadcast, to determine if there are any peers available.
	 */
	public void sendPeer() {
		// send a peer command to the BROADCAST (*.255) for this subnet
		try {
			mUDPTalkBuffer.add("255.255.255.255", UDP_LISTEN_PORT, 1,
					CommandsEnum.SYSTEM_PEER.toString(), null);
		} catch (InterruptedException e) {
			printLogMessage(e.toString());
		}
	}

	/**
	 * Validate if a file exists, is the correct filetype and is of a size
	 * suitable for array loading.
	 * 
	 * @param fileName
	 * @return true if file is ok, false otherwise
	 */
	public boolean validateFile(String fileName) {

		// Create the file object.
		File f = new File(filePath + fileName);

		// Check that the file exists and is readable.
		if (f.exists() && f.canRead()) {

			if (f.getName().endsWith(".iso") && f.length() <= Integer.MAX_VALUE) {
				printLogMessage(filePath + fileName
						+ " is a valid image type.");
				return true;
			}

		}

		return false;
	}

	/**
	 * Calculate how much of a file each peer should process and send a 'LOAD'
	 * command to each peer with this information in.
	 * 
	 * @param fileName
	 * @throws InterruptedException
	 */
	public void splitFile(String fileName) throws InterruptedException {

		// get the file stream
		File image = new File(filePath + fileName);
		int numPeers;
		int fileSize;
		int bytesPerPeer;
		String[][] peers = peerMgr.getAllPeers();

		// get the number of peers
		numPeers = peerMgr.getCount();

		if (numPeers == 0) {
			numPeers = 1;
		}

		// get the file size
		fileSize = (int) image.length();

		printLogMessage("Filesize = " + fileSize + " bytes");

		// calculate how many bytes per peer
		// round down to prevent any missed bytes
		// make the bytes per peer overlap to prevent any missed searches
		double calc = Math.ceil((double) fileSize / (double) numPeers);
		bytesPerPeer = (int) calc;

		int peer = 0;
		int id = this.getID();
		String cmd = CommandsEnum.PROC_LOAD.toString();

		// send each peer a customized LOAD command
		for (int currByte = 0; currByte < fileSize; currByte += bytesPerPeer) {
			mTalkBuffer.add(peers[peer][0], peerMgr.getPort(peers[peer][0]),
					id, cmd, fileName + "," + currByte + "," + bytesPerPeer);
			peer++;
		}
	}

	/**
	 * Send a 'SEARCH' command to all connected peers
	 * 
	 * @param query
	 * @throws InterruptedException
	 */
	public void sendSearch(String query) throws InterruptedException {
		int id = this.getID();

		String[][] allPeers = peerMgr.getAllPeers();

		for (int i = 0; i < allPeers.length; i++) {
			String address = allPeers[i][0];
			mTalkBuffer.add(address, peerMgr.getPort(address), id,
					CommandsEnum.PROC_SEARCH.toString(), query);

			// add an entry to the statemanager
			this.stateMgr.add(id, address);

		}
	}

	/**
	 * Increment and return the current command ID
	 * 
	 * @return
	 */
	private int getID() {
		this.commandID++;

		return this.commandID;
	}

	/**
	 * Processing for a received JOIN command
	 * 
	 * @param ip
	 * @param id
	 */
	private void sysJoin(String ip, String id, String port)
			throws InterruptedException {
		printLogMessage("JOIN received from " + ip );

		// add new peer to the peermanager
		this.peerMgr.addPeer(ip, Integer.valueOf(port));
		// respond with a 'done'
		this.mTalkBuffer.add(ip, peerMgr.getPort(ip), Integer.valueOf(id),
				"DONE", null);
	}

	/**
	 * Processing for a received LEAVE command
	 * 
	 * @param ip
	 * @param id
	 */
	private void sysLeave(String ip) throws InterruptedException {
		// check that the statemanager contains no entries for this IP
		if (this.stateMgr.contains(ip)) {
			// respond with an error, slave must flush out any entries with a
			// COMPLETE or DONE
			this.mTalkBuffer.add(ip, peerMgr.getPort(ip), getID(), "ERROR",
					"1,flush all pending handles with a COMPLETE or DONE");
		} else {
			// respond with a DONE
			this.mTalkBuffer.add(ip, peerMgr.getPort(ip), getID(), "DONE", " ");
			// remove peer from the peer manager
			this.peerMgr.removePeer(ip);
		}
	}

	/**
	 * Processing for a received DONE command
	 * 
	 * @param ip
	 * @param id
	 */
	private void sysDone(String ip, String id) {
		// remove the matching IP and ID from the statemanager
		printLogMessage("DONE received from " + ip + " with ID " + id);
		this.stateMgr.remove(Integer.valueOf(id), ip);
	}

	/**
	 * Processing for a received COMPLETE command
	 * 
	 * @param ip
	 * @param id
	 * @param data
	 */
	private void procComplete(String ip, String id, String data) {
		// remove the matching IP and ID from the statemanager
		printLogMessage("COMPLETE received from " + ip + " with ID " + id);
		this.stateMgr.remove(Integer.valueOf(id), ip);

	}

	/**
	 * Processing for a received RESULT command
	 * 
	 * @param ip
	 * @param id
	 * @param data
	 */
	private void procResult(String ip, String data) {
		// Connects to database and enters results

		printLogMessage("RESULT from " + ip + " " + data);

		String[] resultsArray = data.split(",");

		try {
			for (String d : resultsArray) {
				resultsBuffer.add(d + "," + ip);
			}
		} catch (InterruptedException e) {
			printLogMessage(e.toString());
		}

	}// Close procResult

	/**
	 * Processing for a received ERROR command
	 * 
	 * @param ip
	 * @param id
	 * @param data
	 */
	private void sysError(String ip, String id, String data) {
		// log the error
		// TODO : Error logging
	}

	/**
	 * Set the FID to fileID from JDBC
	 * 
	 * @param fileID
	 * @return
	 */
	public int setFID(String fileName) {
		try {
			if (databaseMgr.openDB()) {
				FID = databaseMgr.setFilePathID(fileName);
			}
		} catch (SQLException e) {
			printLogMessage(e.toString());
		} catch (Exception e) {
			printLogMessage(e.toString());
		}

		return FID;
	}// Close setFID

	/**
	 * Set the SID to searchStringID from JDBC
	 * 
	 * @param fileID
	 * @return
	 */
	public int setSID(String searchString) {
		try {
			if (databaseMgr.openDB()) {
				SID = databaseMgr.setSearchStringID(searchString);
			}
		} catch (SQLException e) {
			printLogMessage(e.toString());
		} catch (Exception e) {
			printLogMessage(e.toString());
		}

		return SID;
	}// Close setSID

	/**
	 * Forces the master processor to quit
	 */
	public void quit() {
		this.running = false;
		MasterProcessor.mp = null;
	}

	/**
	 * Check whether the master processor has already been kicked off
	 * 
	 * @return true if running, false otherwise
	 */
	public boolean getRunning() {
		return this.running;
	}

	/**
	 * Unused command to return the currently logged in user.
	 * 
	 * @return String userID
	 */
	@Deprecated
	public String getUserID() {
		return this.userID;
	}
	
	/**
	 * Outputs a string to the console, pre-cursed by a date time stamp
	 * 
	 * @param s
	 */
	private void printLogMessage(String s){
		
		StringBuilder msg = new StringBuilder();
		DateFormat dateFormat = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss");
		Date date = new Date();
		
		msg.append(dateFormat.format(date));
		msg.append("||");
		msg.append(s);
		msg.append("||");
		
		System.out.println(msg.toString());
		
		
	}
	

}
